package net.xt.myupload.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

@RestController
public class FileUploadController {

    private final String fileLocation = "/upload/";//图片资源访问路径
    private static final String UPLOAD_PATH = "D:\\upload";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 秒传文件
     */
    @PostMapping("/secondUpload")
    public String secondUpload(@RequestParam("file") Part file,
                               HttpServletRequest request) {
        try {
            // 定义上传文件存放的路径
            // 此处为tomcat下的路径，服务重启路径会变化
            String uploadPath = request.getSession().getServletContext().getRealPath(fileLocation);
            File uploadFile = new File(uploadPath);
            if (!uploadFile.isDirectory()) {
                uploadFile.mkdirs();
            }
            file.write(file.getSubmittedFileName());

            // 获取文件的md5，并保存到 redis中
            String fileMd5 = getFileMd5(file.getInputStream());
            String key = "file_" + fileMd5;
            if (redisTemplate.opsForValue().get(key) != null) {
                return "秒传成功";
            } else {
                redisTemplate.opsForValue().set(key, "false");
                redisTemplate.opsForValue().set("block_" + fileMd5, uploadPath);
                return "文件正在上传中";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "文件上传失败" + e.getMessage();
        }
    }

    private static String getFileMd5(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("MD5");
        byte[] hash = digest.digest(inputStream.readAllBytes());

        // 转换成十六进制数，左边使用32个0
        return String.format("%032x", new BigInteger(1, hash));
    }


    /**
     * 分片上传大文件
     *
     * @param chunkSize   每个分片大小
     * @param chunkNumber 当前分片
     * @param md5         文件总MD5
     * @param file        当前分片文件数据
     */
    @RequestMapping("/uploadBig")
    public ResponseEntity<Map<String, String>> uploadBig(Long chunkSize,
                                                         Integer totalNumber,
                                                         Long chunkNumber,
                                                         String md5,
                                                         MultipartFile file)
            throws IOException {
        //文件存放位置
        String destFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
        //上传分片信息存放位置
        String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        //第一次创建分片记录文件
        //创建目录
        File dir = new File(destFile).getParentFile();
        if (!dir.exists()) {
            dir.mkdir();
            //所有分片状态设置为0
            byte[] bytes = new byte[totalNumber];
            Files.write(Path.of(confFile), bytes);
        }

        // 随机分片写入文件
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(destFile, "rw");
             RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
             InputStream inputStream = file.getInputStream()) {
            // 定位到该分片的偏移量
            randomAccessFile.seek(chunkNumber * chunkSize);
            // 写入该分片数据
            randomAccessFile.write(inputStream.readAllBytes());
            // 定位到当前分片状态位置
            randomAccessConfFile.seek(chunkNumber);
            // 设置当前分片上传状态为 1
            randomAccessConfFile.write(1);
        }

        return ResponseEntity.ok(Map.of("path", destFile));
    }

    /**
     * 断点续传大文件
     *
     * @param chunkSize   每个分片大小
     * @param totalNumber 总分片数
     * @param chunkNumber 当前分片
     * @param md5         文件总MD5
     * @param file        当前分片文件数据
     */
    @PostMapping("/breakUpload")
    public ResponseEntity<Map<String, String>> breakUpload(Long chunkSize,
                                                           Integer totalNumber,
                                                           Integer chunkNumber,
                                                           String md5,
                                                           MultipartFile file)
            throws Exception {
        // 确保文件目录存在
        String dirPath = String.format("%s\\%s\\", UPLOAD_PATH, md5);
        // 文件名
        String fileName = String.format("%s.%s", md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
        // 文件存放位置
        String destFilePath = String.format("%s\\%s\\%s", UPLOAD_PATH, md5, fileName);
        // 分片状态信息存放位置
        String confFilePath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);

        File dir = new File(dirPath);
        if (!dir.exists()) {
            dir.mkdirs();
            // 所有分片状态默认设置为 0
            byte[] bytes = new byte[totalNumber];
            Files.write(Path.of(confFilePath), bytes);
        }

        // 随机分片写入文件
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(destFilePath, "rw");
             RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFilePath, "rw");
             InputStream inputStream = file.getInputStream()) {
            // 写入分片数据
            // 定位到该分片的偏移量
            randomAccessFile.seek(chunkNumber * chunkSize);
            // 写入该分片数据
            randomAccessFile.write(inputStream.readAllBytes());

            // 更新分片状态
            // 定位到当前分片状态位置
            randomAccessConfFile.seek(chunkNumber);
            // 设置当前分片状态为 1
            randomAccessConfFile.write(1);
        }

        Map<String, String> response = new HashMap<>();
        response.put("path", destFilePath);
        return ResponseEntity.ok(response);
    }

    /**
     * 获取文件分片状态，检测文件MD5合法性
     *
     * @param md5
     */
    @RequestMapping("/checkFile")
    public ResponseEntity<Map<String, String>> checkFile(String md5) throws Exception {
        String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
        Path path = Path.of(uploadPath);
        // MD5目录不存在，则文件从未上传过
        if (!Files.exists(path.getParent())) {
            return ResponseEntity.ok(Map.of("msg", "文件未上传"));
        }

        //判断文件是否上传成功
        StringBuilder stringBuilder = new StringBuilder();
        byte[] bytes = Files.readAllBytes(path);
        for (byte b : bytes) {
            stringBuilder.append(String.valueOf(b));
        }

        // 所有分片上传完成计算文件MD5
        if (!stringBuilder.toString().contains("0")) {
            File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
            File[] files = file.listFiles();
            String filePath = "";
            for (File f : files) {
                //计算文件MD5是否相等
                if (!f.getName().contains("conf")) {
                    filePath = f.getAbsolutePath();
                    try (InputStream inputStream = new FileInputStream(f)) {
                        String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
                        if (!md5pwd.equalsIgnoreCase(md5)) {
                            return ResponseEntity.ok(Map.of("msg", "文件md5值不匹配"));
                        }
                    }
                }
            }
            return ResponseEntity.ok(
                    Map.of(
                            "path", filePath,
                            "msg", "文件已上传"
                    )
            );
        } else {
            //文件未上传完成，返回每个分片状态，前端将未上传的分片继续上传
            return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
        }
    }

}
